﻿/*
 Microsoft Public License (MS-PL)
 --------------------------------
 * * This license governs use of the accompanying software. If you use the software, you
accept this license. If you do not accept the license, do not use the software.

1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the
same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.

2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.

3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.
 */
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Text.RegularExpressions;
using Quartz;

namespace SD.CoreLibrary.CronExpressionDescripter
{
    /// <summary>
    /// Converts cron expressions into human readable strings.
    /// </summary>
    public class CronExpressionDescriptor
    {
        private readonly char[] _specialCharacters = new char[] { '/', '-', ',', '*' };
        private string _expression;
        private CronDescriptionOptions _options;
        private string[] _expressionParts;
        private bool _parsed;

        public CronExpressionNormalizer CronExpressionValues { get; private set; }

        public string CronExpression
        {
            get
            {
                string resp = "";
                string sec = SecondsSegment.CronExpression;
                string min = MinutesSegment.CronExpression;
                string hour = HoursSegment.CronExpression;
                string dom = DayOfMonthsSegment.CronExpression;
                string mon = MonthsSegment.CronExpression;
                string dow = DayOfWeeksSegment.CronExpression;
                //if (dom.Contains("?"))
                //    dow = dow.Replace("?", "*");
                string year = YearsSegment.CronExpression;

                year = year == "0" ? string.Empty : year;

                return string.Format("{0} {1} {2} {3} {4} {5} {6}", sec, min, hour, dom, mon, dow, year);
            }
        }

        public CronExpressionDescriptor(string expression) : this(expression, CronDescriptionTypeEnum.Full, new CronDescriptionOptions()) { }
        public CronExpressionDescriptor(string expression, CronDescriptionTypeEnum type, CronDescriptionOptions options)
        {
            _expression = expression.ToUpper();
            _options = options;
            _expressionParts = new string[7];
            _parsed = false;
            if (type != CronDescriptionTypeEnum.None)
                GetDescription(type);
        }

        public string GetDescription(CronDescriptionTypeEnum type)
        {
            string description = string.Empty;

            try
            {
                if (!_parsed)
                {
                    CronExpressionValues = new CronExpressionNormalizer(_expression, _options);
                    _expressionParts = CronExpressionValues.Parse();


                    SecondsSegment = new CronSecondsSegment(_expressionParts[0]);
                    MinutesSegment = new CronMinutesSegment(_expressionParts[1]);
                    HoursSegment = new CronHoursSegment(_expressionParts[2]);
                    DayOfMonthsSegment = new CronDayOfMonthsSegment(_expressionParts[3]);
                    MonthsSegment = new CronMonthsSegment(_expressionParts[4]);
                    DayOfWeeksSegment = new CronDayOfWeeksSegment(_expressionParts[5]);
                    YearsSegment = new CronYearsSegment(_expressionParts[6]);

                    _parsed = true;
                }

                switch (type)
                {
                    case CronDescriptionTypeEnum.Full:
                        description = GetFullDescription();
                        break;
                    case CronDescriptionTypeEnum.TimeOfDay:
                        description = GetTimeOfDayDescription();
                        break;
                    case CronDescriptionTypeEnum.Hours:
                        description = GetHoursDescription();
                        break;
                    case CronDescriptionTypeEnum.Minutes:
                        description = GetMinutesDescription();
                        break;
                    case CronDescriptionTypeEnum.Seconds:
                        description = GetSecondsDescription();
                        break;
                    case CronDescriptionTypeEnum.DayOfMonth:
                        description = GetDayOfMonthDescription();
                        break;
                    case CronDescriptionTypeEnum.Month:
                        description = GetMonthDescription();
                        break;
                    case CronDescriptionTypeEnum.DayOfWeek:
                        description = GetDayOfWeekDescription();
                        break;
                    case CronDescriptionTypeEnum.Year:
                        description = GetYearDescription();
                        break;
                    default:
                        description = GetSecondsDescription();
                        break;
                }
            }
            catch (Exception ex)
            {
                if (!_options.ThrowExceptionOnParseError)
                {
                    description = ex.Message;
                }
                else
                {
                    throw;
                }
            }
            return description;
        }
        public string Description
        {
            get { return GetDescription(CronDescriptionTypeEnum.Full); }
        }

        public CronSecondsSegment SecondsSegment { get; set; }
        public CronMinutesSegment MinutesSegment { get; set; }
        public CronHoursSegment HoursSegment { get; set; }
        public CronMonthsSegment MonthsSegment { get; set; }
        public CronYearsSegment YearsSegment { get; set; }
        public CronDayOfWeeksSegment DayOfWeeksSegment { get; set; }
        public CronDayOfMonthsSegment DayOfMonthsSegment { get; set; }

        private static CronExpressionDescriptor Descripter { get; set; }

        protected string GetFullDescription()
        {
            string description;

            try
            {
                string timeSegment = GetTimeOfDayDescription();
                string dayOfMonthDesc = GetDayOfMonthDescription();
                string monthDesc = GetMonthDescription();
                string dayOfWeekDesc = GetDayOfWeekDescription();
                string yearDesc = GetYearDescription();

                description = string.Format("{0}{1}{2}{3}",
                    timeSegment,
                    (_expressionParts[3] == "*" ? dayOfWeekDesc : dayOfMonthDesc),
                    monthDesc,
                    yearDesc);

                description = TransformVerbosity(description);
                description = TransformCase(description);
                description = CleanExtraSpaces(description);
            }
            catch (Exception ex)
            {
                description = "An error occured when generating the expression description.  Check the cron expression syntax.";
                if (_options.ThrowExceptionOnParseError)
                {
                    throw new FormatException(description, ex);
                }
            }
            return description;
        }

        protected string GetTimeOfDayDescription()
        {
            //string secondsExpression = _expressionParts[0];
            //string minuteExpression = _expressionParts[1];
            //string hourExpression = _expressionParts[2];

            string secondsExpression = SecondsSegment.CronExpression;
            string minuteExpression = MinutesSegment.CronExpression;
            string hourExpression = HoursSegment.CronExpression;

            StringBuilder description = new StringBuilder();

            //handle special cases first
            if (minuteExpression.IndexOfAny(_specialCharacters) == -1
                && hourExpression.IndexOfAny(_specialCharacters) == -1
                && secondsExpression.IndexOfAny(_specialCharacters) == -1)
            {
                //specific time of day (i.e. 10 14)
                description.Append("At ").Append(FormatTime(_options.ConvertHourToAMPM, hourExpression, minuteExpression, secondsExpression));
                //SecondsSegment = new CronSecondsSegment(_expressionParts[0]);
                //MinutesSegment = new CronMinutesSegment(_expressionParts[1]);
                //HoursSegment = new CronHoursSegment(_expressionParts[2]);
            }

            else
            {
                //default time description
                string secondsDescription = GetSecondsDescription();
                string minutesDescription = GetMinutesDescription();
                string hoursDescription = GetHoursDescription();

                description.Append(secondsDescription);

                if (description.Length > 0)
                {
                    description.Append(", ");
                }

                description.Append(minutesDescription);

                if (description.Length > 0)
                {
                    description.Append(", ");
                }

                description.Append(hoursDescription);
            }


            return description.ToString();
        }

        protected string GetSecondsDescription()
        {
            //SecondsSegment = new CronSecondsSegment(_expressionParts[0]);
            string description = SecondsSegment.GetSegmentDescription();

            //string description = SecondsSegment.GetSegmentDescription(
            //     "every second",
            //   (s => s.PadLeft(2, '0')),
            //   (s => string.Format("every {0} seconds", s)),
            //   ((s, p) => string.Format("seconds {0} through {1} past the minute", s, p)),
            //   (s => string.Format("at {0} seconds past the minute", s)));
            return description;
        }

        protected string GetMinutesDescription()
        {
            //MinutesSegment = new CronMinutesSegment(_expressionParts[1]);
            string description = MinutesSegment.GetSegmentDescription();

            //string description = MinutesSegment.GetSegmentDescription(
            //      "every minute",
            //    (s => s.PadLeft(2, '0')),
            //    (s => string.Format("every {0} minutes", s.PadLeft(2, '0'))),
            //    ((s, p) => string.Format("minutes {0} through {1} past the hour", s, p)),
            //    (s => s == "0" ? string.Empty : string.Format("at {0} minutes past the hour", s)));
            return description;
        }

        protected string GetHoursDescription()
        {
            //HoursSegment = new CronHoursSegment(_expressionParts[2]);
            string description = HoursSegment.GetSegmentDescription();
            //string description = HoursSegment.GetSegmentDescription(
            //     "every hour",
            //   (s => FormatTime(_options.ConvertHourToAMPM, s, "0")),
            //   (s => string.Format("every {0} hours", s.PadLeft(2, '0'))),
            //   ((s, p) => string.Format("between {0} and {1}", FormatTime(_options.ConvertHourToAMPM, s, "0"), FormatTime(_options.ConvertHourToAMPM, p, "0"))),
            //   (s => string.Format("at {0}", s)));

            return description;
        }

        protected string GetDayOfWeekDescription()
        {
            //DayOfWeeksSegment = new CronDayOfWeeksSegment(_expressionParts[5]);
            string description = DayOfWeeksSegment.GetSegmentDescription();
            //string description = DayOfWeeksSegment.GetSegmentDescription(
            //    ", every day",
            //    (s => FormatDayOfTheWeek(s)),
            //    (s => string.Format(", every {0} days of the week", s)),
            //    ((s, p) => string.Format(", {0} through {1}", FormatDayOfTheWeek(s), FormatDayOfTheWeek(p))),
            //    (s => string.Format(", on {0}", s)));

            return description;
        }

        protected string GetMonthDescription()
        {
            //MonthsSegment = new CronMonthsSegment(_expressionParts[4]);
            string description = MonthsSegment.GetSegmentDescription();
            //string description = MonthsSegment.GetSegmentDescription(
            //    string.Empty,
            //   (s => new DateTime(DateTime.Now.Year, Convert.ToInt32(s), 1).ToString("MMMM", new CultureInfo("en-US"))),
            //   (s => string.Format(", every {0} months", s)),
            //   ((s, p) => string.Format(", {0} through {1}", s, p)),
            //   (s => string.Format(", only in {0}", s)));
            return description;
        }

        protected string GetDayOfMonthDescription()
        {
            //DayOfMonthsSegment = new CronDayOfMonthsSegment(_expressionParts[3].Replace("?", "*"));
            string description = DayOfMonthsSegment.GetSegmentDescription();
            //description = DayOfMonthsSegment.GetSegmentDescription(
            //            ", every day",
            //            (s => s),
            //            (s => s == "1" ? ", every day" : string.Format(", every {0} days",s),
            //            ((s, p) => string.Format(", between day {0} and {1} of the month", s, p)),
            //            (s => string.Format(", on day {0} of the month", s)));
            return description;
        }

        protected string GetYearDescription()
        {
            //YearsSegment = new CronYearsSegment(_expressionParts[6]);
            string description = YearsSegment.GetSegmentDescription();
            //string description = YearsSegment.GetSegmentDescription(
            //    string.Empty,
            //   (s => new DateTime(Convert.ToInt32(s), 1, 1).ToString("yyyy")),
            //   (s => string.Format(", every {0} years", s)),
            //   ((s, p) => string.Format(", {0} through {1}", s, p)),
            //   (s => string.Format(", only in {0}", s)));
            return description;
        }

        //protected string GetSegmentDescription(string expression,
        //    string allDescription,
        //    Func<string, string> singleItemDescription,
        //    Func<string, string> intervalDescriptionFormat,
        //    Func<string, string> betweenDescriptionFormat,
        //    Func<string, string> descriptionFormat)
        //{
        //    string description = null;

        //    if (string.IsNullOrEmpty(expression))
        //    {
        //        description = string.Empty;
        //    }
        //    else if (expression == "*")
        //    {
        //        description = allDescription;
        //    }
        //    else if (expression.IndexOfAny(new char[] { '/', '-', ',' }) == -1)
        //    {
        //        description = string.Format(descriptionFormat(expression), singleItemDescription(expression));
        //    }
        //    else if (expression.Contains("/"))
        //    {
        //        string[] segments = expression.Split('/');
        //        description = string.Format(intervalDescriptionFormat(segments[1]), singleItemDescription(segments[1]));

        //        //interval contains 'between' piece (i.e. 2-59/3 )
        //        if (segments[0].Contains("-"))
        //        {
        //            string betweenSegmentOfInterval = segments[0];
        //            string[] betweensegments = betweenSegmentOfInterval.Split('-');
        //            description += ", " + string.Format(betweenDescriptionFormat(betweenSegmentOfInterval), singleItemDescription(betweensegments[0]), singleItemDescription(betweensegments[1]));
        //        }
        //    }
        //    else if (expression.Contains(","))
        //    {
        //        string[] segments = expression.Split(',');

        //        string descriptionContent = string.Empty;
        //        for (int i = 0; i < segments.Length; i++)
        //        {
        //            if (i > 0 && segments.Length > 2)
        //            {
        //                descriptionContent += ",";

        //                if (i < segments.Length - 1)
        //                {
        //                    descriptionContent += " ";
        //                }
        //            }

        //            if (i > 0 && segments.Length > 1 && (i == segments.Length - 1 || segments.Length == 2))
        //            {
        //                descriptionContent += " and ";
        //            }

        //            descriptionContent += singleItemDescription(segments[i]);
        //        }

        //        description = string.Format(descriptionFormat(expression), descriptionContent);
        //    }
        //    else if (expression.Contains("-"))
        //    {
        //        string[] segments = expression.Split('-');
        //        description = string.Format(betweenDescriptionFormat(expression), singleItemDescription(segments[0]), singleItemDescription(segments[1]));
        //    }


        //    return description;
        //}

        public static string FormatTime(bool convertAMPM, string hourExpression, string minuteExpression, string secondExpression = "")
        {
            string resp = "";
            int hour;
            bool ok = int.TryParse(hourExpression, out hour);
            if (ok)
            {
                resp = HourToPMAM(convertAMPM, hour, minuteExpression, secondExpression);
            }
            else
            {
                if (hourExpression.Contains("-"))
                {
                    string[] parts = hourExpression.Split('-');
                    if (parts.Length == 2)
                    {
                        int h1 = Convert.ToInt32(parts[0]);
                        int h2 = Convert.ToInt32(parts[1]);
                        resp = string.Format("between {0} and {1}",
                            HourToPMAM(convertAMPM, h1, minuteExpression, secondExpression),
                            HourToPMAM(convertAMPM, h2, minuteExpression, secondExpression));
                    }
                }

            }
            return resp;
        }

        public static string FormatDayOfTheWeek(string dayOfTheWeek)
        {
            DayOfWeek dow = (DayOfWeek)(Convert.ToInt32(dayOfTheWeek));
            return dow.ToString();
        }

        private static string HourToPMAM(bool convertToAMPM, int hour, string minuteExpression, string secondExpression)
        {
            string resp = "";
            string hourString = "";
            string amPM = "";
            if (convertToAMPM)
            {
                amPM = hour >= 12 ? "PM" : "AM";
                if (hour > 12)
                {
                    hour -= 12;
                }
            }
            string minute = Convert.ToInt32(minuteExpression).ToString();
            string second = string.Empty;
            if (!string.IsNullOrEmpty(secondExpression))
            {
                second = string.Concat(":", Convert.ToInt32(secondExpression).ToString().PadLeft(2, '0'));
            }
            hourString = hour.ToString().PadLeft(2, '0');
            resp = string.Format("{0}:{1}{2} {3}", hourString, minute.PadLeft(2, '0'), second, amPM);
            return resp;
        }

        protected string TransformVerbosity(string description)
        {
            if (!_options.Verbose)
            {
                description = description.Replace(", every minute", string.Empty);
                description = description.Replace(", every hour", string.Empty);
                description = description.Replace(", every day", string.Empty);
            }

            return description;
        }

        protected string TransformCase(string description)
        {
            switch (_options.CasingType)
            {
                case CasingTypeEnum.Sentence:
                    description = string.Concat(char.ToUpper(description[0]), description.Substring(1));
                    break;
                case CasingTypeEnum.Title:
                    description = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(description);
                    break;
                default:
                    description = description.ToLower();
                    break;

            }
            return description;
        }

        protected string CleanExtraSpaces(string description)
        {
            string cleanedString = System.Text.RegularExpressions.Regex.Replace(description, @"\s+", " ");
            cleanedString = cleanedString.Replace(" , ", ", ");
            return cleanedString;
        }

        public static string GetDescription(string expression)
        {
            return GetDescription(expression, new CronDescriptionOptions());
        }

        public static string GetDescription(string expression, CronDescriptionOptions options)
        {
            Descripter = new CronExpressionDescriptor(expression, CronDescriptionTypeEnum.None, options);
            return Descripter.GetDescription(CronDescriptionTypeEnum.Full);
        }
    }
}